01 ImageLoader 基础框架

01 基础框架
02 请求队列
03 三级缓存
04 图片加载
05 常见问题
06 项目源码

搭建项目,无非两个思路。一般情况下都是由上而下、从抽象到具体、先搭框架再实现细节,但是对人员的综合素养要求很高。我们选择另一条相反的思路,由下而上从细节到框架。等到对设计模式、组织架构等知识有一定的理解和实践经验之后,再在实际项目中采用第一种思路。

现在我们就拿使用范围广、难度也适中的图片加载器(ImageLoader)作为训练项目。

一、核心功能


如果仅仅是为了实现加载图片的功能,那么下面的代码完全满足:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
/**
* 图片加载类
* @author xianxiaotao
*/
public class ImageLoader {
// UI Handler
private Handler mUiHandler = new Handler(Looper.getMainLooper());
/**
* 加载图片并显示
* @param url 图片地址
* @param imageView 显示图片控件
*/
public void displayImage(final String url, final ImageView imageView) {
if (TextUtils.isEmpty(url) || null == imageView) {
return;
}
new Thread(new Runnable() {
@Override
public void run() {
final Bitmap bitmap = downloadImage(url);
if (null == bitmap) {
return;
}
updateImageView(imageView, bitmap);
}
}).start();
}
/**
* 加载网络图片
* @param imageUrl 图片地址
* @return Bitmap
*/
private Bitmap downloadImage(String imageUrl) {
if (TextUtils.isEmpty(imageUrl)) {
return null;
}
Bitmap bitmap = null;
try {
URL url = new URL(imageUrl);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
bitmap = BitmapFactory.decodeStream(conn.getInputStream());
conn.disconnect();
} catch (Exception e) {
e.printStackTrace();
}
return bitmap;
}
/**
* 更新视图控件,显示图片
* @param imageView ImageView
* @param bitmap Bitmap
*/
private void updateImageView(final ImageView imageView, final Bitmap bitmap) {
if (null == imageView || null == bitmap) {
return;
}
mUiHandler.post(new Runnable() {
@Override
public void run() {
imageView.setImageBitmap(bitmap);
}
});
}
}

测试代码:

1
2
3
String url = "https://mtl.gzhuibei.com/images/img/19816/1.jpg";
ImageLoader loader = new ImageLoader();
loader.displayImage(url, imageView);

效果图:

实际开发过程中,往往不是只加载一张图就完事了,而是以列表的形式展现给用户,所以我们就需要考虑图片加载速度,节省用户流量,防止大图片引发 OOM,多线程等问题。

我们先对上面的示例增加内存缓存和线程池功能,内存缓存直接使用 ANDROID 提供的 LruCache 类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
public class ImageLoader {
// 图片内存缓存
private LruCache<String, Bitmap> mImageCache;
// 线程池,线程数量为 CPU 的数量
private ExecutorService mExecutorService = Executors.newFixedThreadPool(
Runtime.getRuntime().availableProcessors());
// UI Handler
private Handler mUiHandler = new Handler(Looper.getMainLooper());
public ImageLoader() {
initImageCache();
}
/**
* 初始化缓存大小:取四分之一的可用内存作为缓存。
*/
private void initImageCache() {
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
int cacheSize = maxMemory / 4;
mImageCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
}
};
}
/**
* 加载图片并显示
* @param url 图片地址
* @param imageView 显示图片控件
*/
public void displayImage(final String url, final ImageView imageView) {
if (TextUtils.isEmpty(url) || null == imageView) {
return;
}
// 先从内存中读取
Bitmap bmp = mImageCache.get(url);
if (null != bmp) {
imageView.setImageBitmap(bmp);
return;
}
// 内存中没有,去加载网络图片
imageView.setTag(url);
mExecutorService.submit(new Runnable() {
@Override
public void run() {
final Bitmap bitmap = downloadImage(url);
if (null == bitmap) {
return;
}
if (url.equals(imageView.getTag())) {
updateImageView(imageView, bitmap);
}
mImageCache.put(url, bitmap);
}
});
}
/**
* 加载网络图片
* @param imageUrl 图片地址
* @return Bitmap
*/
private Bitmap downloadImage(String imageUrl) {
if (TextUtils.isEmpty(imageUrl)) {
return null;
}
Bitmap bitmap = null;
try {
URL url = new URL(imageUrl);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
bitmap = BitmapFactory.decodeStream(conn.getInputStream());
conn.disconnect();
} catch (Exception e) {
e.printStackTrace();
}
return bitmap;
}
/**
* 更新视图控件,显示图片
* @param imageView ImageView
* @param bitmap Bitmap
*/
private void updateImageView(final ImageView imageView, final Bitmap bitmap) {
if (null == imageView || null == bitmap) {
return;
}
mUiHandler.post(new Runnable() {
@Override
public void run() {
imageView.setImageBitmap(bitmap);
}
});
}
}

到目前为止,我们的 ImageLoader 中含有线程池、缓存系统、网络请求等,很消耗资源。因此,没有理由让它构造多个实例。使用单例模式来保证 ImageLoader 类只有一个实例存在。代码改造如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class ImageLoader {
// ImageLoader 实例
private static ImageLoader sInstance;
private ImageLoader() {}
/**
* 获取ImageLoader单例
* @return ImageLoader
*/
public static ImageLoader getInstance() {
if (sInstance == null) {
synchronized (ImageLoader.class) {
if (sInstance == null) {
sInstance = new ImageLoader();
}
}
}
return sInstance;
}
// 代码省略...
}

现在我们再审视代码,发现 ImageLoader 的内存缓存、多线程、网络加载等功能之间严重耦合,缓存相关逻辑、图片加载显示逻辑、多线程切换逻辑严重混淆在一起。如果我们要对内存缓存功能进行扩展优化,实现二级缓存,那么我们只能在 ImageLoader 类修改,还要保证不影响其他功能。这样,随着功能的增多,ImageLoader 类会越来越大,代码也越来越复杂,更别谈扩展性、灵活性。

必须对 ImageLoader 进行拆分,让各个功能独立出来。比如内存初始化、存取逻辑可以单独抽出缓存类来 ImageCache;下载网络图片逻辑也是一样处理。

二、职责拆分


ImageCache

首先我们先把 ImageCache 类拆分出来,它负责缓存相关的职责,比如初始化缓存大小,并提供存(put)、取(get)、移除(remove)接口。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 缓存类
public class ImageCache {
private static final int ONE_KB = 1024;
private static final int MAX_MEMORY_PERCENT = 4;
// 图片内存缓存
private LruCache<String, Bitmap> mImageCache;
public ImageCache() {
initImageCache();
}
// 初始化缓存大小:取四分之一的可用内存作为缓存。
private void initImageCache() {
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / ONE_KB);
int cacheSize = maxMemory / MAX_MEMORY_PERCENT;
mImageCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
return bitmap.getRowBytes() * bitmap.getHeight() / ONE_KB;
}
};
}
public void put(String key, Bitmap value) {
mImageCache.put(key, value);
}
public Bitmap get(String key) {
return mImageCache.get(key);
}
public void remove(String key) {
mImageCache.remove(key);
}
}

其中 LruCache 是线程安全地类,所以在多线程环境中,没有必要对 put、get 等使用 synchronized 关键字进行同步。

这样一来,ImageLoader 只需要持有 ImageCache 的对象即可,不必关心其中的细节。因为 ImageLoader 是单例对象,那么持有的缓存也是只有一个实例的。那么 ImageLoader 可以向其他功能提供统一的访问接口 getCache()。ImageLoader 需要的修改的代码如下:

1
2
3
4
5
6
7
8
9
10
public class ImageLoader {
// 图片内存缓存
private ImageCache mImageCache = new ImageCache();
public ImageCache getCache() {
return mImageCache;
}
// 代码省略...
}

RequestThreadPool

接下来把异步请求代码块移植到 RequestThreadPool 类中,由 RequestThreadPool 专门负责异步下载。这样 ImageLoader 类就非常简单了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class ImageLoader {
// ImageLoader 实例
private static ImageLoader sInstance;
// 图片内存缓存
private ImageCache mImageCache = new ImageCache();
// 线程池
private RequestThreadPool mThreadPool = new RequestThreadPool();
private ImageLoader() {}
/**
* 获取ImageLoader单例
* @return ImageLoader
*/
public static ImageLoader getInstance() {
if (sInstance == null) {
synchronized (ImageLoader.class) {
if (sInstance == null) {
sInstance = new ImageLoader();
}
}
}
return sInstance;
}
public ImageCache getCache() {
return mImageCache;
}
/**
* 加载图片并显示
* @param url 图片地址
* @param imageView 显示图片控件
*/
public void displayImage(String url, ImageView imageView) {
mThreadPool.addRequest(url, imageView);
}
}

毕竟大部分业务逻辑全迁移到 RequestThreadPool 类中,而 ImageLoader 只作为程序入口。RequestThreadPool 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
public class RequestThreadPool {
// 线程池,线程数量为 CPU 的数量
private ExecutorService mExecutorService = Executors.newFixedThreadPool(
Runtime.getRuntime().availableProcessors());
// UI Handler
private Handler mUiHandler = new Handler(Looper.getMainLooper());
public void addRequest(final String url, final ImageView imageView) {
if (TextUtils.isEmpty(url) || null == imageView) {
return;
}
// 先从内存中读取
Bitmap bmp = ImageLoader.getInstance().getCache().get(url);
if (null != bmp) {
imageView.setImageBitmap(bmp);
return;
}
// 内存中没有,去加载网络图片
imageView.setTag(url);
mExecutorService.submit(new Runnable() {
@Override
public void run() {
final Bitmap bitmap = downloadImage(url);
if (null == bitmap) {
return;
}
if (url.equals(imageView.getTag())) {
updateImageView(imageView, bitmap);
}
ImageLoader.getInstance().getCache().put(url, bitmap);
}
});
}
/**
* 加载网络图片
* @param imageUrl 图片地址
* @return Bitmap
*/
private Bitmap downloadImage(String imageUrl) {
if (TextUtils.isEmpty(imageUrl)) {
return null;
}
Bitmap bitmap = null;
try {
URL url = new URL(imageUrl);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
bitmap = BitmapFactory.decodeStream(conn.getInputStream());
conn.disconnect();
} catch (Exception e) {
e.printStackTrace();
}
return bitmap;
}
/**
* 更新视图控件,显示图片
* @param imageView ImageView
* @param bitmap Bitmap
*/
private void updateImageView(final ImageView imageView, final Bitmap bitmap) {
if (null == imageView || null == bitmap) {
return;
}
mUiHandler.post(new Runnable() {
@Override
public void run() {
imageView.setImageBitmap(bitmap);
}
});
}
}

Loader

到这里结束了吗?没有,还可以继续拆,拆 RequestThreadPool 类,将其中下载模块拆成一个新类 Loader:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
public class Loader {
public void load(String url, ImageView imageView) {
// 先从内存中读取
Bitmap bitmap = ImageLoader.getInstance().getCache().get(url);
// 没有就去加载网络图片
if (null == bitmap) {
bitmap = downloadImage(url);
if (null != bitmap) {
ImageLoader.getInstance().getCache().put(request, bitmap);
}
}
// 通知界面更新
deliveryToUIThread(imageView, bitmap, url);
}
/**
* 加载网络图片
* @param imageUrl 图片地址
* @return Bitmap
*/
private Bitmap downloadImage(String imageUrl) {
if (TextUtils.isEmpty(imageUrl)) {
return null;
}
Bitmap bitmap = null;
try {
URL url = new URL(imageUrl);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
bitmap = BitmapFactory.decodeStream(conn.getInputStream());
conn.disconnect();
} catch (Exception e) {
e.printStackTrace();
}
return bitmap;
}
/**
* 更新视图控件,显示图片
* @param imageView ImageView
* @param bitmap Bitmap
*/
private void deliveryToUIThread(final ImageView imageView,
final Bitmap bitmap, final String url) {
if (null != imageView) {
imageView.post(new Runnable() {
@Override
public void run() {
updateImageView(imageView, bitmap, url);
}
});
}
}
/**
* 更新ImageView
*/
private void updateImageView(ImageView imageView, Bitmap result, String url) {
if (result != null && imageView != null && !TextUtils.isEmpty(url)
&& url.equals(imageView.getTag())) {
imageView.setImageBitmap(result);
}
}
}

从上述代码中可以看出 Loader 加载图片的过程有如下几个步骤:

  • 判断缓存中是否含有该图片;
  • 如果有则将图片直接投递到UI线程,并且更新UI;
  • 如果没有缓存,则从对应的地方获取到图片,并且将图片缓存起来,然后再将结果投递给UI线程,更新UI;

既然 Loader 类承担了下载图片职责,那么 RequestThreadPool 类就只有调度异步任务的职责了,从 ImageLoader 接收到一个图片加载请求之后,创建/分配线程,调用 Loader 提供的接口执行图片加载程序。其代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class RequestThreadPool {
// 线程池,线程数量为 CPU 的数量
private ExecutorService mExecutorService = Executors.newFixedThreadPool(
Runtime.getRuntime().availableProcessors());
public void addRequest(final String url, final ImageView imageView) {
if (TextUtils.isEmpty(url) || null == imageView) {
return;
}
// 内存中没有,去加载网络图片
imageView.setTag(url);
mExecutorService.submit(new Runnable() {
@Override
public void run() {
new Loader().load(url, imageView);
}
});
}
}

这里有个问题,线程池每执行一个异步任务,都会创建新的 Loader 对象。很明显这是不合理的,我们可以使用单例模式来优化 Loader 类。代码不再给出,请大家参照 ImageLoader 的单例形式自行编写。

由一个类拆成 4 个类,虽然避免了将来 ImageLoader 类的臃肿膨胀,但也使得整个系统的变得复杂,这样做值得吗?我只想说,这才刚开始。欲知后事如何,请看下篇文章。